'use strict'

const { expect } = require('chai')
const log = require('log').get('serverless:test')
const awsRequest = require('@serverless/test/aws-request')
const CloudFormationService = require('aws-sdk').CloudFormation
const fixtures = require('../../fixtures/programmatic')

const { confirmCloudWatchLogs } = require('../../utils/misc')
const {
  deployService,
  removeService,
  fetch,
} = require('../../utils/integration')

describe('AWS - API Gateway Integration Test', function () {
  this.timeout(1000 * 60 * 10) // Involves time-taking deploys
  let serviceName
  let endpoint
  let stackName
  let serviceDir
  let updateConfig
  let apiKey
  let isDeployed = false
  const stage = 'dev'

  const resolveEndpoint = async () => {
    const result = await awsRequest(CloudFormationService, 'describeStacks', {
      StackName: stackName,
    })
    const endpointOutput = result.Stacks[0].Outputs.find(
      (output) => output.OutputKey === 'ServiceEndpoint',
    ).OutputValue
    endpoint = endpointOutput.match(
      /https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/,
    )[0]
  }

  before(async () => {
    const serviceData = await fixtures.setup('api-gateway-extended')
    ;({ servicePath: serviceDir, updateConfig } = serviceData)
    serviceName = serviceData.serviceConfig.service
    apiKey = `${serviceName}-api-key-1`
    stackName = `${serviceName}-${stage}`
    await deployService(serviceDir)
    isDeployed = true
    return resolveEndpoint()
  })

  after(async () => {
    if (!isDeployed) return
    log.notice('Removing service...')
    await removeService(serviceDir)
  })

  describe('Minimal Setup', () => {
    const expectedMessage = 'Hello from API Gateway! - (minimal)'

    it('should expose an accessible GET HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}`

      return fetch(testEndpoint, { method: 'GET' })
        .then((response) => response.json())
        .then((json) => expect(json.message).to.equal(expectedMessage))
    })

    it('should expose an accessible POST HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/minimal-1`

      return fetch(testEndpoint, { method: 'POST' })
        .then((response) => response.json())
        .then((json) => expect(json.message).to.equal(expectedMessage))
    })

    it('should expose an accessible PUT HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/minimal-2`

      return fetch(testEndpoint, { method: 'PUT' })
        .then((response) => response.json())
        .then((json) => expect(json.message).to.equal(expectedMessage))
    })

    it('should expose an accessible DELETE HTTP endpoint', async () => {
      const testEndpoint = `${endpoint}/minimal-3`

      return fetch(testEndpoint, { method: 'DELETE' })
        .then((response) => response.json())
        .then((json) => expect(json.message).to.equal(expectedMessage))
    })
  })

  describe('CORS', () => {
    it('should setup simple CORS support via cors: true config', async () => {
      const testEndpoint = `${endpoint}/simple-cors`

      return fetch(testEndpoint, { method: 'OPTIONS' }).then((response) => {
        const headers = response.headers
        const allowHeaders = [
          'Content-Type',
          'X-Amz-Date',
          'Authorization',
          'X-Api-Key',
          'X-Amz-Security-Token',
          'X-Amz-User-Agent',
          'X-Amzn-Trace-Id',
        ].join(',')
        expect(headers.get('access-control-allow-headers')).to.equal(
          allowHeaders,
        )
        expect(headers.get('access-control-allow-methods')).to.equal(
          'OPTIONS,GET',
        )
        expect(headers.get('access-control-allow-credentials')).to.equal(null)
        expect(headers.get('access-control-allow-origin')).to.equal('*')
      })
    })

    it('should setup CORS support with complex object config', async () => {
      const testEndpoint = `${endpoint}/complex-cors`

      return fetch(testEndpoint, { method: 'OPTIONS' }).then((response) => {
        const headers = response.headers
        const allowHeaders = [
          'Content-Type',
          'X-Amz-Date',
          'Authorization',
          'X-Api-Key',
          'X-Amz-Security-Token',
          'X-Amz-User-Agent',
          'X-Amzn-Trace-Id',
        ].join(',')
        expect(headers.get('access-control-allow-headers')).to.equal(
          allowHeaders,
        )
        expect(headers.get('access-control-allow-methods')).to.equal(
          'OPTIONS,GET',
        )
        expect(headers.get('access-control-allow-credentials')).to.equal('true')
        expect(headers.get('access-control-allow-origin')).to.equal('*')
      })
    })
  })

  describe('Custom Authorizers', () => {
    let testEndpoint

    before(() => {
      testEndpoint = `${endpoint}/custom-auth`
    })

    it('should reject requests without authorization', async () => {
      return fetch(testEndpoint).then((response) => {
        expect(response.status).to.equal(401)
      })
    })

    it('should reject requests with wrong authorization', async () => {
      return fetch(testEndpoint, {
        headers: { Authorization: 'Bearer ShouldNotBeAuthorized' },
      }).then((response) => {
        expect(response.status).to.equal(401)
      })
    })

    it('should authorize requests with correct authorization', async () => {
      return fetch(testEndpoint, {
        headers: { Authorization: 'Bearer ShouldBeAuthorized' },
      })
        .then((response) => response.json())
        .then((json) => {
          expect(json.message).to.equal(
            'Hello from API Gateway! - (customAuthorizers)',
          )
          expect(json.event.requestContext.authorizer.principalId).to.equal(
            'SomeRandomId',
          )
          expect(json.event.headers.Authorization).to.equal(
            'Bearer ShouldBeAuthorized',
          )
        })
    })
  })

  describe('API Keys', () => {
    let testEndpoint
    let startTime

    before(() => {
      testEndpoint = `${endpoint}/api-keys`
      startTime = Date.now()
    })

    it('should succeed if correct API key is given', async function self() {
      const response = await fetch(testEndpoint, {
        headers: { 'X-API-Key': apiKey },
      })
      const result = await response.json()
      // API Key may take a moment to propagate, retry
      if (response.status === 403 && startTime > Date.now() - 1000 * 60 * 3) {
        log.notice('API Key rejected, retry')
        return self()
      }
      expect(response.status).to.equal(200)
      expect(result.message).to.equal('Hello from API Gateway! - (apiKeys)')
      return null
    })

    it('should reject a request with an invalid API Key', async () => {
      return fetch(testEndpoint).then((response) => {
        expect(response.status).to.equal(403)
      })
    })
  })

  describe('Using stage specific configuration', () => {
    before(async () => {
      await updateConfig({
        provider: {
          tags: {
            foo: 'bar',
            baz: 'qux',
          },
          tracing: {
            apiGateway: true,
          },
          logs: {
            restApi: true,
          },
        },
      })
      await deployService(serviceDir)
    })

    it('should update the stage without service interruptions', async () => {
      // re-using the endpoint from the "minimal" test case
      const testEndpoint = `${endpoint}`

      return confirmCloudWatchLogs(
        `/aws/api-gateway/${stackName}`,
        () =>
          fetch(`${testEndpoint}`, { method: 'GET' })
            .then((response) => response.json())
            // Confirm that APIGW responds as expected
            .then((json) =>
              expect(json.message).to.equal(
                'Hello from API Gateway! - (minimal)',
              ),
            ),
        // Confirm that CloudWatch logs for APIGW are written
      ).then((events) => expect(events.length > 0).to.equal(true))
    })
  })

  describe('Integration Lambda Timeout', () => {
    it('should result with 504 status code', async () =>
      fetch(`${endpoint}/integration-lambda-timeout`).then((response) =>
        expect(response.status).to.equal(504),
      ))
  })
})
